using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Threading;
using System.Windows.Forms;
using System.IO;

namespace AVR109Boot
{
    /// <summary>
    /// This class contains an implementation of Atmel's AVR109 bootloader protocol 
    /// as well as a serial interface to communicate with a microcontroller via its UART. 
    /// </summary>
    public class AVR109Bootloader
    {
        #region Datamembers
        private enum Instructions
        {
            CheckAutoIncrementStatus,
            SetAddress,
            ChipErase,
            CheckBlockLoadSupport,
            StartBlockLoad,
            StartBlockRead,
            ReadProgramMemory,
            WriteProgramMemoryBytes,
            PageWrite,
            WriteEEPROMMemory,
            ReadEEPROMMemory,
            WriteLockBits,
            ReadLockBits,
            ReadLowFuseBits,
            ReadHighFuseBits,
            ReadExtendedFuseBits,
            GetProgrammerIdentifier,
            GetSoftwareVersion,
            GetSignatureBytes,
            Synchronization,
            ExitBootloader
        }
        private class Command
        {
            public Instructions Instruction;
            public byte[] Parameters;
        }

        private Command[] CommandQueue;
        private bool CommandReady = false;

        private Form Parent;
        /// <summary>
        /// buffer of bytes returned from the micro-controller
        /// </summary>
        public byte[] ReturnedData;


        private string errors = "";
        /// <summary>
        /// get all errors that occured during this instance of AVR109Bootloader 
        /// </summary>
        public string Errors
        {
            get { return errors; }
        }

        /// <summary>
        /// The current task that this class is working on
        /// </summary>
        public string Task
        {
            get { return task; }
        }
        private string task;

        private int tasknum = 0;
        private int totaltasks = 1;
        /// <summary>
        /// The percentage of complete tasks compared to the total tasks
        /// </summary>
        public double PercentageCompletion
        {
            get
            {
                return percentage_completion;
            }
        }
        private double percentage_completion = 0.0;

        /// <summary>
        /// The amount of time in millisecond to wait after sending a command for a response.
        /// </summary>
        public int timeout = 200;
        System.Timers.Timer ThreadMonitor;
        private ThreadStart SerialPortThreadStart;
        private Thread SerialPortThread;
        private bool CommandLoopBusy = false;
        /// <summary>
        /// if set to true the class is busy communicating with the micro and cannot accept any further commands
        /// </summary>
        public bool Busy
        {
            get { return CommandLoopBusy; }
        }

        private SerialPort sp;
        private string outlog = "";
        private string inlog = "";

        private int NumBytesToRead;
        private int WatchDog = 0;

        private ChipData chipdata;

        public bool SaveLogs = true;
        #endregion
        #region Constructors, initialization, and clean up
        /// <summary>
        /// Create a new bootloader object
        /// </summary>
        /// <param name="ParentForm">The form that has created this class</param>
        /// <param name="chip">Define which chip you are using. See class AVRChipData for definitions.</param>
        /// <param name="portName">name of serial port ex: "COM3"</param>
        /// <param name="baudRate">baud rate ex 57600</param>

        public AVR109Bootloader(Form ParentForm, ChipData chip, string portName, int baudRate)
        {
            this.chipdata = chip;
            this.Parent = ParentForm;
            try
            {
                sp = new SerialPort(portName, baudRate);
                sp.Open();
            }
            catch (Exception e)
            {
                errors += e.Message + "\r\n";
            }
            init();
        }
        /// <summary>
        /// Create a new bootloader object
        /// </summary>
        /// <param name="ParentForm">The form that has created this class</param>
        /// <param name="chip">Define which chip you are using. See class AVRChipData for definitions.</param>
        /// <param name="portName">name of serial port ex: "COM3"</param>
        /// <param name="baudRate">baud rate ex 57600</param>
        /// <param name="parity">one of the System.IO.Ports.Parity values</param>
        /// <param name="dataBits">number of databits per serial transmission</param>
        public AVR109Bootloader(Form ParentForm, ChipData chip, string portName, int baudRate, Parity parity, int dataBits)
        {
            this.chipdata = chip;
            this.Parent = ParentForm;
            try
            {
                sp = new SerialPort(portName, baudRate, parity, dataBits);
                sp.Open();
            }
            catch (Exception e)
            {
                errors += e.Message + "\r\n";
            }
            init();
        }
        /// <summary>
        /// Create a new bootloader object
        /// </summary>
        /// <param name="ParentForm">The form that has created this class</param>
        /// <param name="chip">Define which chip you are using. See class AVRChipData for definitions.</param>
        /// <param name="portName">name of serial port ex: "COM3"</param>
        /// <param name="baudRate">baud rate ex 57600</param>
        /// <param name="parity">one of the System.IO.Ports.Parity values</param>
        /// <param name="dataBits">number of databits per serial transmission</param>
        /// <param name="SaveLogs">Specify whether or not to save the serial port's incoming and outgoing bytes to log files</param>
        public AVR109Bootloader(Form ParentForm, ChipData chip, string portName, int baudRate, Parity parity, int dataBits, bool SaveLogs)
        {
            this.SaveLogs = SaveLogs;
            this.chipdata = chip;
            this.Parent = ParentForm;
            try
            {
                sp = new SerialPort(portName, baudRate, parity, dataBits);
                sp.Open();
            }
            catch (Exception e)
            {
                errors += e.Message + "\r\n";
            }
            init();
        }
        private void init()
        {

            SerialPortThreadStart = new ThreadStart(CommandLoop);
            SerialPortThread = new Thread(SerialPortThreadStart);
            ThreadMonitor = new System.Timers.Timer(timeout);
            ThreadMonitor.Enabled = true;
            ThreadMonitor.Elapsed += new System.Timers.ElapsedEventHandler(ThreadMonitor_Elapsed);
            CommandQueue = new Command[0];
            SerialPortThread.Start();
            if (SaveLogs)
            {
                File.Delete("inlog.text");
                File.Delete("outlog.text");
            }

        }
        /// <summary>
        /// Attempt to connect to the micro controller's bootloader, and bypass its main program.
        /// </summary>
        /// <returns>true on success, or false on failure</returns>
        public bool Connect()
        {
            if (sp.IsOpen)
            {
                sp.Write(new byte[] { (byte)'L' }, 0, 1);
                string s = sp.ReadExisting();
                if (s.Contains("L"))
                {
                    return true;
                }
            }
            else
            {
                errors += "Cannot connect.  Serial Port is not open.\r\n";
            }
            return false;
        }
        /// <summary>
        /// clean up the running threads
        /// </summary>
        public void Dispose()
        {
            ThreadMonitor.Stop();
            SerialPortThread.Abort();
            sp.Close();
            File.WriteAllText("inlog.text", inlog);
            File.WriteAllText("outlog.text", outlog);
        }
        #endregion
        #region Events
        /// <summary>
        /// Thrown when a the microcontroller has returned all requested data
        /// </summary>
        public delegate void DataReceivedEventHandler(object sender, DataReceivedEventArgs e);
        /// <summary>
        /// Thrown when a the microcontroller has returned all requested data
        /// </summary>
        public event DataReceivedEventHandler DataReceived;
        /// <summary>
        /// DataReceived event args.  Contains the returned data.
        /// </summary>
        public class DataReceivedEventArgs : System.EventArgs
        {
            /// <summary>
            /// The requested data
            /// </summary>
            public byte[] ReturnedData;
            /// <summary>
            /// DataReceived event args constructor
            /// </summary>
            /// <param name="ReturnedData">The data returned by the micro controller</param>
            public DataReceivedEventArgs(ref byte[] ReturnedData)
            {
                this.ReturnedData = ReturnedData;
            }
        }
        /// <summary>
        /// Thrown when a the microprocessor has been issued a command and took too long to respond
        /// </summary>
        public delegate void TimeoutEventHandler(object sender, EventArgs e);
        /// <summary>
        /// Thrown when a the microprocessor has been issued a command and took too long to respond
        /// </summary>
        public event TimeoutEventHandler Timeout;
        #endregion        
        #region Private methods
        private void ThreadMonitor_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            ThreadMonitor.Interval = timeout;
            if (WatchDog == 1)
            {
                errors += "timeout\r\n";
                CommandLoopBusy = false;
                CommandReady = false;
                CommandQueue = new Command[0];//clear the command queue
                SerialPortThread.Abort();
                SerialPortThread = new Thread(SerialPortThreadStart);
                SerialPortThread.Start();//try to restart 
                totaltasks = 0;
                tasknum = -1;
                updatePercentageCompletion();
                WatchDog = 2;
                Parent.Invoke(Timeout, new Object[] { this, new EventArgs() });
            }
            //WatchDog++; //commented out for debugging
        }
        private void CommandLoop()
        {

            string buffer;
            while (true)
            {
                if (!CommandReady)
                {
                    WatchDog = 0;
                    task = "Idle";
                    totaltasks = 0;
                    CommandLoopBusy = false;
                    Thread.Sleep(1);
                    continue;//dont do anything further this iteration there is no command to process
                }
                else
                {
                    CommandLoopBusy = true;
                }
                foreach (Command Command in CommandQueue)
                {
                    WatchDog = 0;
                    if (sp.BytesToRead > 0)//flush any bytes stuck in the port before issuing a new command.
                    {
                        buffer = ReadBinaryData();
                    }
                    switch (Command.Instruction)
                    {
                        
                        case Instructions.CheckAutoIncrementStatus:
                            SPout((byte)'a');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            if (!buffer.Contains("Y"))
                            {
                                errors += "Micro controller does not autoincrement.\r\n";
                            }
                            break;
                        case Instructions.CheckBlockLoadSupport:
                            SPout((byte)'b');
                            while (sp.BytesToRead < 3) ;
                            buffer = ReadBinaryData();
                            if (buffer[0] == 'Y')
                            {
                                ReturnedData = new byte[2];
                                ReturnedData[0] = (byte)buffer[1];//blocksize MSB
                                ReturnedData[1] = (byte)buffer[2];//blocksize LSB
                                Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            }
                            break;
                        case Instructions.ChipErase:
                            SPout((byte)'e');
                            ThreadMonitor.Interval = 3000;//give the micro time to erase the flash and respond
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            if (buffer[0] != '\r')
                            {
                                errors += "Error erasing flash\r\n";
                            }
                            break;
                        case Instructions.GetProgrammerIdentifier:
                            SPout((byte)'S');
                            while (sp.BytesToRead < 7) ;
                            buffer = ReadBinaryData();
                            if (buffer != "AVRBOOT")
                            {
                                errors += "invalid software identifier\r\n";
                            }
                            break;
                        case Instructions.GetSignatureBytes:
                            SPout((byte)'s');
                            while (sp.BytesToRead < 3) ;
                            buffer = ReadBinaryData();
                            ReturnedData = new byte[3];
                            ReturnedData[0] = (byte)buffer[0];
                            ReturnedData[1] = (byte)buffer[1];
                            ReturnedData[2] = (byte)buffer[2];
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });         
                            break;
                        case Instructions.GetSoftwareVersion:
                            SPout((byte)'V');
                            while (sp.BytesToRead < 2) ;
                            buffer = ReadBinaryData();

                            ReturnedData = new byte[2];
                            ReturnedData[0] = (byte)buffer[0];
                            ReturnedData[1] = (byte)buffer[1];
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;
                        case Instructions.PageWrite:
                            SPout((byte)'m');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            if (buffer[0] == '?')
                            {
                                errors += "Page write error: Attempted to page write into bootloader area. No write was performed\r\n";
                            }
                            else if (buffer[0] != '\r')
                            {
                                errors += "Page write error\r\n";
                            }
                            break;
                        case Instructions.ReadEEPROMMemory:
                            for (int i = 0; i < NumBytesToRead; i++)
                            {
                                task = "Reading byte " + i + " of " + NumBytesToRead + " from eeprom\r\n";
                                WatchDog = 0;
                                SPout((byte)'d');
                                while (sp.BytesToRead < 1) ;
                                buffer = ReadBinaryData();
                                ReturnedData = new byte[1];
                                ReturnedData[0] = (byte)buffer[0];
                            }
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;
                        case Instructions.ReadExtendedFuseBits:
                            SPout((byte)'Q');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            ReturnedData = new byte[1];
                            ReturnedData[0] = (byte)buffer[0];
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;
                        case Instructions.ReadLowFuseBits:
                            SPout((byte)'F');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            ReturnedData = new byte[1];
                            ReturnedData[0] = (byte)buffer[0];
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;
                        case Instructions.ReadHighFuseBits:
                            SPout((byte)'N');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            ReturnedData = new byte[1];
                            ReturnedData[0] = (byte)buffer[0];
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;
                        case Instructions.ReadLockBits:
                            SPout((byte)'r');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            ReturnedData = new byte[1];
                            ReturnedData[0] = (byte)buffer[0];
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;

                        case Instructions.StartBlockRead:
                            SPout((byte)'g');
                            SPout(Command.Parameters[0]);
                            SPout(Command.Parameters[1]);
                            int size = (Command.Parameters[0] << 8) + Command.Parameters[1];
                            while (sp.BytesToRead < size) ;
                            buffer = ReadBinaryData();
                            ReturnedData = new byte[size];
                            for (int i = 0; i < size; i++)
                            {
                                ReturnedData[i] = (byte)buffer[i];
                            }
                            break;
                        case Instructions.Synchronization:
                            SPout(0x1b);
                            while (sp.BytesToRead < 1) ;
                            ReturnedData = new byte[] { 0x00 };
                            buffer = ReadBinaryData();
                            if (buffer[0] == '?')
                            {
                                errors += "out of sync\r\n";
                            }
                            break;
                        case Instructions.WriteEEPROMMemory:
                            totaltasks = Command.Parameters.Length;
                            int num = 0;
                            foreach (byte parameter in Command.Parameters)
                            {
                                task = "Writing byte + " + num + " of " + totaltasks + "bytes.";
                                WatchDog = 0;
                                SPout((byte)'D');
                                SPout(parameter);
                                while (sp.BytesToRead < 1) ;

                                buffer = ReadBinaryData();
                                if (buffer[0] != '\r')
                                {
                                    errors += "Attempted to write to EEPROM.  Microcontroller did not respond properly\r\n";
                                }
                                num++;
                                updatePercentageCompletion();
                            }
                            break;
                        case Instructions.StartBlockLoad:
                            int pagesize = chipdata.PageSize * 2;//times 2 and divide by 2 are here because AVR architecture uses 16 bit words 
                            int numpages = (Command.Parameters.Length / 2) / chipdata.PageSize;
                            if ((Command.Parameters.Length / 2) % chipdata.PageSize != 0)
                            {
                                numpages++;
                            }
                            totaltasks = Command.Parameters.Length;
                            for (int i = 0; i < numpages; i++)
                            {
                                SPout((byte)'B');
                                SPout((byte)(((chipdata.PageSize * 2)>>8)&0xFF));
                                SPout((byte)((chipdata.PageSize * 2)&0xFF));
                                SPout((byte)'F');
                                for (int j = 0; j < chipdata.PageSize * 2; j++)//load the rest of the data
                                {
                                    int address = j + (chipdata.PageSize * i * 2);
                                    task = "Writing byte " + address + " of " + Command.Parameters.Length;

                                    if (address < Command.Parameters.Length)
                                    {
                                        if (address % 2 == 0)
                                        {
                                            SPout(Command.Parameters[address + 1]);
                                        }
                                        else
                                        {
                                            SPout(Command.Parameters[address - 1]);
                                        }
                                    }
                                    else
                                    {
                                        SPout(0xFF);
                                    }
                                    WatchDog = 0;
                                    //Thread.Sleep(10);
                                    updatePercentageCompletion();

                                }
                                while (sp.BytesToRead < 1) ;
                                buffer = ReadBinaryData();
                            }
                            break;
                        case Instructions.WriteLockBits:
                            if (Command.Parameters.Length == 1)
                            {
                                SPout((byte)'l');
                                SPout(Command.Parameters[0]);
                                while (sp.BytesToRead < 1) ;
                                buffer = ReadBinaryData();
                                if (buffer[0] != '\r')
                                {
                                    errors += "Attempted to write lock bits.  Micro did not respond properly.\r\n";
                                }
                            }
                            else
                            {
                                errors += "Write lock bits requires a 1 byte parameter\r\n";
                            }
                            break;
                        case Instructions.SetAddress:
                            if (Command.Parameters.Length == 2)
                            {
                                task = "Setting address to " + string.Format("{0:x2}{1:x2}", Command.Parameters[0], Command.Parameters[1]);
                                SPout((byte)'A');
                                SPout(Command.Parameters[0]);
                                SPout(Command.Parameters[1]);
                                while (sp.BytesToRead < 1) ;
                                buffer = ReadBinaryData();
                                if (buffer[0] != '\r')
                                {
                                    errors += "After attempting to set address the micro controller did not reply correctly\r\n";
                                }
                            }
                            else
                            {
                                errors += "SetAddress command requires 2 byte parameters (16 bit word address)\r\n";
                            }
                            break; 
                        case Instructions.ReadProgramMemory:
                            totaltasks = NumBytesToRead / 2;
                            ReturnedData = new byte[NumBytesToRead];
                            for (int i = 0; i < NumBytesToRead; i++)
                            {
                                task = "Reading byte " + i + " of " + NumBytesToRead + " bytes from program memory";
                                WatchDog = 0;
                                SPout((byte)'R');
                                while (sp.BytesToRead < 2) ;
                                buffer = ReadBinaryData();
                                ReturnedData[i] = (byte)buffer[0];
                                ReturnedData[i + 1] = (byte)buffer[1];
                                i++;
                                updatePercentageCompletion();
                            }
                            NumBytesToRead = 0;
                            ThreadMonitor.Enabled = false;//for large quantities of data the thread needs more time
                            Parent.Invoke(DataReceived, new object[] { this, new DataReceivedEventArgs(ref ReturnedData) });
                            break;
                        case Instructions.WriteProgramMemoryBytes://this writes the low and high bytes of each program memory word
                            totaltasks = Command.Parameters.Length/2;
                            for (int i = 0; i < Command.Parameters.Length; i++)
                            {
                                task = "Writing byte " + i + " of " + totaltasks + " bytes to program memory";
                                WatchDog = 0;
                                SPout((byte)'c');
                                SPout(Command.Parameters[i + 1]);//write low byte first
                                while (sp.BytesToRead < 1) ;
                                buffer = ReadBinaryData();
                                if (buffer[0] != '\r')//avr should respond with this otherwise there is an error
                                {
                                    errors += "Error while writing to microcontroller\r\n";
                                    break;
                                }
                                SPout((byte)'C');
                                SPout(Command.Parameters[i]);//high byte
                                while (sp.BytesToRead < 1) ;
                                buffer = ReadBinaryData();
                                if (buffer[0] != '\r')//avr should respond with this otherwise there is an error
                                {
                                    errors += "Error while writing to microcontroller\r\n";
                                    break;
                                }
                                i++;
                                updatePercentageCompletion();
                            }
                            break;
                        case Instructions.ExitBootloader:
                            SPout((byte)'E');
                            while (sp.BytesToRead < 1) ;
                            buffer = ReadBinaryData();
                            if (buffer[0] != '\r')//avr should respond with this otherwise there is an error
                            {
                                errors += "Error trying to exit bootloader\r\n";
                            }
                            break;
                    }
                    totaltasks = 0;
                    tasknum = -1;
                    updatePercentageCompletion();
                }
                CommandReady = false;
                CommandQueue = new Command[0];//clear the command queue
                ThreadMonitor.Enabled = true;
            }
        }
        private void updatePercentageCompletion()
        {
            if (totaltasks != 0)
            {
                tasknum++;
                percentage_completion = (double)tasknum / (double)totaltasks * 100;
                if (percentage_completion > 100.0)
                {
                    percentage_completion = 100.0;
                }
            }
            else
            {
                percentage_completion = 0.0;
            }
        }
        private string ReadBinaryData()//workaround to read in binary data properly to a string, there is some problem with encoding? 
        {
            string returnVal="";
            for (int i = 0; i <= sp.BytesToRead; i++)
            {
                byte incomingByte = (byte)sp.ReadByte();
                char incomingChar = (char)incomingByte;
                returnVal += incomingChar;

            }
            inlog += returnVal;

            return returnVal;
        }
        private void SPout(byte output)
        {
            outlog += output;

            sp.Write(new byte[] { output }, 0, 1);
        }
        #endregion
        #region public methods
        /// <summary>
        /// Read a number of bytes starting at address.  The returned data is stored in byte[] ReturnedData.
        /// To recieve the buffer the moment the read is complete handle the DataReceivedEvent.
        /// </summary>
        /// <param name="numbytes">The number of bytes to read</param>
        /// <param name="address">Starting address</param>
        public void ReadProgramMemory(int numbytes, UInt16 address)
        {
            if (sp.IsOpen)
            {
                if (!CommandLoopBusy)
                {
                    NumBytesToRead = numbytes;

                    Command SetAddress = new Command();
                    SetAddress.Instruction = Instructions.SetAddress;
                    SetAddress.Parameters = new byte[2];
                    SetAddress.Parameters[1] = (byte)((address >> 8) & 0xFF);
                    SetAddress.Parameters[0] = (byte)(address & 0xFF);

                    Command ReadProgramMemory = new Command();
                    ReadProgramMemory.Instruction = Instructions.ReadProgramMemory;
                    ReadProgramMemory.Parameters = null;

                    CommandQueue = new Command[2] { SetAddress, ReadProgramMemory };

                    CommandReady = true;
                }
                else
                {
                    errors += "Unable to read program memory.  The program is currently busy.\r\n";
                }
            }
            else
            {
                errors += "Unable to read program memory.  The serial port is not open.\r\n";
            }
        }

        /// <summary>
        /// Erase all flash bytes.  Required before programming.
        /// </summary>
        public void EraseFlash()
        {
            if (sp.IsOpen)
            {
                if (!CommandLoopBusy)
                {
                    Command EraseFlash = new Command();
                    EraseFlash.Instruction = Instructions.ChipErase;
                    
                    CommandQueue = new Command[1]{EraseFlash};
                    CommandReady = true;

                }
                else
                {
                    errors += "Unable to erase flash.  The program is currently busy.\r\n";
                }
            }
            else
            {
                errors += "Unable to erase flash.  The serial port is not open.\r\n";
            }
        }
        /// <summary>
        /// Write all bytes of the program in byte[] data using block mode which is faster than WriteProgramMemory()
        /// </summary>
        /// <param name="data"></param>
        public void WriteProgramMemoryBlockMode(byte[] data, bool verify)
        {
            if (sp.IsOpen)
            {
                if (!CommandLoopBusy)
                {
                    if (verify)
                    {
                        CommandQueue = new Command[5];
                    }
                    else
                    {
                        CommandQueue = new Command[3];
                    }
                    Command SetAddress = new Command();
                    SetAddress.Parameters = new byte[2];
                    SetAddress.Parameters[0] = (byte)(0);
                    SetAddress.Parameters[1] = (byte)(0);
                    SetAddress.Instruction = Instructions.SetAddress;

                    Command ChipErase = new Command();
                    ChipErase.Instruction = Instructions.ChipErase;

                    Command BlockWrite = new Command();
                    BlockWrite.Instruction = Instructions.StartBlockLoad;
                    BlockWrite.Parameters = data;

                    CommandQueue[0] = ChipErase;
                    CommandQueue[1] = SetAddress;
                    CommandQueue[2] = BlockWrite;
                    if (verify)
                    {
                        CommandQueue[3] = SetAddress;

                        Command ReadProgramMemory = new Command();
                        ReadProgramMemory.Instruction = Instructions.ReadProgramMemory;
                        CommandQueue[4] = ReadProgramMemory;
                        NumBytesToRead = BlockWrite.Parameters.Length;
                    }
                    CommandReady = true;
                }
                else
                {
                    errors += "Unable to write program memory.  The program is currently busy.\r\n";
                }
            }
            else
            {
                errors += "Unable to read program memory.  The serial port is not open.\r\n";
            }
        }
        /// <summary>
        /// Write data to the flash program memory.
        /// </summary>
        /// <param name="data">The data to write</param>
        public void WriteProgramMemory(byte[] data)
        {
            if (sp.IsOpen)
            {
                if (!CommandLoopBusy)
                {
                    int pagesize = chipdata.PageSize*2;//times 2 and divide by 2 are here because AVR architecture uses 16 bit words 
                    int numpages = (data.Length/2) / chipdata.PageSize;
                    if ((data.Length/2) % chipdata.PageSize != 0)
                    {
                        numpages++;
                    }

                    CommandQueue = new Command[4 * numpages];
                    

                    Command PageWrite = new Command();//PageErase and PageWrite commands can be reused for each page since it requires no parameters
                    PageWrite.Instruction = Instructions.PageWrite;

                    Command[] WriteData = new Command[numpages];

                    Command[] SetAddress = new Command[numpages];


                    for (int i = 0; i < numpages; i++)
                    {
                        WriteData[i] = new Command();
                        WriteData[i].Parameters = new byte[chipdata.PageSize*2];
                        WriteData[i].Instruction = Instructions.WriteProgramMemoryBytes;

                        SetAddress[i] = new Command();
                        SetAddress[i].Parameters = new byte[2];
                        SetAddress[i].Parameters[0] = (byte)(((chipdata.PageSize * i) >> 8) & 0xFF);
                        SetAddress[i].Parameters[1] = (byte)((chipdata.PageSize * i) & 0xFF);
                        SetAddress[i].Instruction = Instructions.SetAddress;

                        for (int j = 0; j < chipdata.PageSize*2; j++)
                        {
                            if (2*chipdata.PageSize*i + j < data.Length)
                            {
                                WriteData[i].Parameters[j] = data[2*chipdata.PageSize * i + j];
                            }
                            else
                            {
                                //there is no more data fill with blanks
                                WriteData[i].Parameters[j] = 0xFF;
                            }
                        }
                        CommandQueue[i * 4 + 0] = SetAddress[i];
                        CommandQueue[i * 4 + 1] = WriteData[i];
                        CommandQueue[i * 4 + 2] = SetAddress[i];
                        CommandQueue[i * 4 + 3] = PageWrite;
                    }


                    CommandReady = true;
                }
                else
                {
                    errors += "Unable to write program memory.  The program is currently busy.\r\n";
                }
            }
            else
            {
                errors += "Unable to read program memory.  The serial port is not open.\r\n";
            }
        }
        /// <summary>
        /// Make the bootloader goto the chip's application
        /// </summary>
        public void ExitBootloader()
        {
            if (sp.IsOpen)
            {
                if (!CommandLoopBusy)
                {
                    Command RestartChip = new Command();
                    RestartChip.Instruction = Instructions.ExitBootloader;
                    CommandQueue = new Command[] { RestartChip };
                    CommandReady = true;
                }
            }
        }
        #endregion
    }
}

